package deformedGrid.painter;


import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.geom.GeneralPath;
import java.awt.geom.Rectangle2D;
import java.util.Arrays;

public class Tile {

	private double[][] oldGrid;
	private double[][] newGrid;
	private int[][] newPoints;
	private double[][] oldPoints;
	private int[] oldPoints_rgb;
	private GeneralPath shape;
	private final int[] imageRGB;
	int width_imageRGB;
	int height_imageRGB;
	private boolean acutely =false;
	/**
	 * @param r
	 * @return
	 * @see java.awt.geom.Path2D#intersects(java.awt.geom.Rectangle2D)
	 */
	public final boolean intersects(Rectangle2D r) {
		return shape.intersects(r);
	}
	
	
	public Shape getShape(){
		return shape;
	}


	public Tile(int[] imageRGB,int height_imageRGB, int width_imageRGB) {
		this.imageRGB = imageRGB;
		this.height_imageRGB = height_imageRGB;
		this.width_imageRGB = width_imageRGB;
	}

	
	private synchronized void check(){
		if (shape == null) {
			throw new NullPointerException("call update first");
		}
		if (!acutely) {
			calculate();
		}
	}
	
	
	private void calculate(){
		acutely = true;
		double[][] oldGrid = this.oldGrid;
		double[][] newGrid = this.newGrid;
		int width_imageRGB = this.width_imageRGB;
		int height_imageRGB = this.height_imageRGB;
		Rectangle r =shape.getBounds();
		int width = r.width;
		int height = r.height;
		int x1 = r.x;
		int y1 = r.y;
		int x2 = x1 + width;
		int y2 = y1 + height;
		int[][] newPoints = new int[width*height][2];
		double[][] oldPoints  = new  double[width*height][];
		int[] oldPoints_rgb = new int[width*height];
		int counter =0;
		double x_old;
		double y_old;
		for (int x = x1; x <= x2; x++) {
			for (int y = y1; y <= y2; y++) {
				if (shape.contains(x, y)) {
					newPoints[counter][0] = x;
					newPoints[counter][1] = y;
					oldPoints[counter] = affinePullBack(newPoints[counter], newGrid, oldGrid);
					x_old=oldPoints[counter][0];
					y_old=oldPoints[counter][1];
					//if data is invalid do nothing
					if (!(x_old >=width_imageRGB -1 || x_old<0 ||y_old >=height_imageRGB -1 || y_old<0 )) {
						oldPoints_rgb[counter] =getsrcColorBilinear(x_old, y_old);
						counter++;
					}
				}
			}
		}
		this.newPoints = Arrays.copyOf(newPoints, counter);
		this.oldPoints = Arrays.copyOf(oldPoints, counter);
		this.oldPoints_rgb = Arrays.copyOf(oldPoints_rgb, counter);
	}
	
	
	/**
	 * 
	 * @param oldGrid  size 4x2
	 * @param newGrid	size 4x2
	 */
	public void update(double[][] oldGrid, double[][] newGrid) {
		acutely = false;
		shape = new GeneralPath(GeneralPath.WIND_NON_ZERO, 4);
		shape.moveTo(newGrid[0][0], newGrid[0][1]);
		shape.lineTo(newGrid[1][0], newGrid[1][1]);
		shape.lineTo(newGrid[2][0], newGrid[2][1]);
		shape.lineTo(newGrid[3][0], newGrid[3][1]);
		shape.closePath();
		this.oldGrid = oldGrid;
		this.newGrid = newGrid;
		
	}
	
	
	
	

	/**
	 * 
	 * @param boundsX 
	 * @param boundsY 
	 * @param colorMap [boundsX*boundsY][2]
	 */
	public void writeInColorMap(double[][] colorMap, int boundsX, int boundsY){
		check();
		int[][] newPoints = this.newPoints;
		double[][] oldPoints  = this.oldPoints;
		for (int i = 0; i < oldPoints.length; i++) {
			if (newPoints[i][1]>=0 &&newPoints[i][0]>=0 && newPoints[i][0] < boundsX && newPoints[i][1] < boundsY) {
				colorMap[newPoints[i][0] + newPoints[i][1] * boundsX] = oldPoints[i];
			}
		}
	}
	
	
	/**
	 * 
	 * @param rgbArray 
	 * @param boundsX 
	 * @param boundsY 
	 * @param colorMap [boundsX*boundsY][2]
	 * @param r 
	 */
	public void writeInColorArray(int[] rgbArray, Rectangle r){
		check();
		int[][] newPoints = this.newPoints;
		double[][] oldPoints  = this.oldPoints;
		for (int i = 0; i < oldPoints.length; i++) {
			if (r.contains(newPoints[i][0],newPoints[i][1])) {
				rgbArray[(newPoints[i][0]-r.x) + (newPoints[i][1]-r.y) * r.width] = oldPoints_rgb[i];
			}
		}
	}




	/**
	 * Pulls v from a p based grid back to a q based
	 * @param v
	 * @param p
	 * @param q
	 * @return
	 */
	private static final double[] affinePullBack(int[] v, double[][] p, double[][] q){
		
		// compute weights and pStar
		double[] weights=new double[p.length];
		double sumWeight=0;
		double pStarX=0;
		double pStarY=0;
		double qStarX=0;
		double qStarY=0;
		
		for (int i = 0; i < weights.length; i++) {
			if ((p[i][0]==v[0]) && (p[i][1]==v[1])) return q[i];
			weights[i]=1.0/((p[i][0]-v[0])*(p[i][0]-v[0])+(p[i][1]-v[1])*(p[i][1]-v[1]));
			pStarX+=weights[i]*p[i][0];
			pStarY+=weights[i]*p[i][1];
			qStarX+=weights[i]*q[i][0];
			qStarY+=weights[i]*q[i][1];
			sumWeight+=weights[i];
		}
		pStarX /= sumWeight;
		pStarY /= sumWeight;
		qStarX /= sumWeight;
		qStarY /= sumWeight;
		//compute the Aj 
		//at first compute the sum pHat_i^t*weights_i*pHat_i
		double inverseWeightedpHatSum11=0;
		double inverseWeightedpHatSum12=0;
		double inverseWeightedpHatSum21=0;
		double inverseWeightedpHatSum22=0;
		for (int i = 0; i < weights.length; i++) {
			//note the diagonal entries 11 and 22 are exchanged -> to get the inverse
			inverseWeightedpHatSum22+=weights[i]*(p[i][0]-pStarX)*(p[i][0]-pStarX);
			inverseWeightedpHatSum12+=weights[i]*(p[i][0]-pStarX)*(p[i][1]-pStarY);
			inverseWeightedpHatSum21+=weights[i]*(p[i][1]-pStarY)*(p[i][0]-pStarX);
			inverseWeightedpHatSum11+=weights[i]*(p[i][1]-pStarY)*(p[i][1]-pStarY);
		
		}
		inverseWeightedpHatSum12=-inverseWeightedpHatSum12;
		inverseWeightedpHatSum21=-inverseWeightedpHatSum21;
		//invert the inverseWeightedpHatSum
		double determinantOfWeightedpHatSum=inverseWeightedpHatSum11*inverseWeightedpHatSum22
				-inverseWeightedpHatSum12*inverseWeightedpHatSum21;
		inverseWeightedpHatSum11/=determinantOfWeightedpHatSum;
		inverseWeightedpHatSum12/=determinantOfWeightedpHatSum;
		inverseWeightedpHatSum21/=determinantOfWeightedpHatSum;
		inverseWeightedpHatSum22/=determinantOfWeightedpHatSum;
		
		
		double A_j = 0;
		double[] f = new double[2];
		f[0]=qStarX; //x value for the returned GridPoint
		f[1]=qStarY; //y value for the returned GridPoint
		for (int j=0; j<weights.length;j++){
			A_j=weights[j]
					*((v[0]-pStarX)
							*(inverseWeightedpHatSum11*(p[j][0]-pStarX)
							+inverseWeightedpHatSum12*(p[j][1]-pStarY))
					+(v[1]-pStarY)
							*(inverseWeightedpHatSum21*(p[j][0]-pStarX)
							+inverseWeightedpHatSum22*(p[j][1]-pStarY)));
			f[0]+=A_j*(q[j][0]-qStarX);
			f[1]+=A_j*(q[j][1]-qStarY);			
		}
		return f;
	}
	
	private int getsrcColorBilinear(double x, double y){
		int width_imageRGB = this.width_imageRGB;
		int height_imageRGB = this.height_imageRGB;
		int[] imageRGB = this.imageRGB;
		if (x >=width_imageRGB -1 || x<0 ||y >=height_imageRGB -1 || y<0 ) {
			return 0;
		}
		int x1 = (int) Math.floor(x);
		int y1 = (int) Math.floor(y);
		int x2 = (int) Math.ceil (x);
		int y2 = (int) Math.ceil (y);
		x-= x1;
		y-= y1;
		
		int ul = imageRGB[x1+ y1*width_imageRGB];
		int ur = imageRGB[x2+ y1*width_imageRGB];
		int dl = imageRGB[x1+ y2*width_imageRGB];
		int dr = imageRGB[x2+ y2*width_imageRGB];	
		int red =(int)
				(((ul >> 16) & 0xFF)*(1-x)*(1-y)+
				((ur >> 16) & 0xFF)*(x)*(1-y)+
				((dl >> 16) & 0xFF)*(1-x)*(y)+
				((dr >> 16) & 0xFF)*(x)*(y));
		int green =(int)
				(((ul >> 8) & 0xFF)*(1-x)*(1-y)+
				((ur >> 8) & 0xFF)*(x)*(1-y)+
				((dl >> 8) & 0xFF)*(1-x)*(y)+
				((dr >> 8) & 0xFF)*(x)*(y));
		int blue =(int)
				(((ul>> 0) & 0xFF)*(1-x)*(1-y)+
				((ur >> 0) & 0xFF)*(x)*(1-y)+
				((dl >> 0) & 0xFF)*(1-x)*(y)+
				((dr >> 0) & 0xFF)*(x)*(y));
		red = red <255 ? red : 255;
		red = red >=0 ? red : 0;
		green = green <255 ? green : 255;
		green = green >=0 ? green : 0;
		blue = blue <255 ? blue : 255;
		blue = blue >=0 ? blue : 0;
		return((255 & 0xFF) << 24) |  //alpha is 255
        ((red & 0xFF) << 16) |
        ((green & 0xFF) << 8)  |
        ((blue & 0xFF) << 0);
	}
	
}
